package com.gitee.wsl.compose.chart.xy.line.stairstep


import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.unit.Constraints
import com.gitee.wsl.common.ui.draw.style.AreaStyle
import com.gitee.wsl.common.ui.draw.style.LineStyle
import com.gitee.wsl.common.ui.draw.style.fillRectangle
import com.gitee.wsl.common.ui.ext.path.lineTo
import com.gitee.wsl.common.ui.ext.path.moveTo
import com.gitee.wsl.compose.chart.KoalaPlotTheme
import com.gitee.wsl.compose.chart.SelectCheck
import com.gitee.wsl.compose.chart.xy.SymbolSlate
import com.gitee.wsl.compose.chart.xy.model.PointEntry
import com.gitee.wsl.compose.chart.xy.XYGraphScope

import com.gitee.wsl.compose.chart.xy.line.area.model.AreaBaseline
import com.gitee.wsl.compose.chart.xy.line.GeneralLinePlot
import com.gitee.wsl.compose.chart.xy.Symbols
import com.gitee.wsl.compose.chart.xy.line.LinePlot
import com.gitee.wsl.compose.chart.xy.line.LinePlotSlate





/**
 * An XY Chart that draws series as points and stairsteps between points.
 * @param X The type of the x-axis values
 * @param Y The type of the y-axis values
 * @param data Data series to plot.
 * @param lineStyle Style to use for the line that connects the data points.
 * @param areaStyle Style to use for filling the area between the line and the 0-cross of the y-axis, or the
 *  y-axis value closest to 0 if the axis does not include 0. If null, no area will be drawn.
 *  [lineStyle] must also be non-null for the area to be drawn.
 * each PointEntry having the same x-axis value.
 * @param areaBaseline Baseline location for the area. Must be not be null if areaStyle and lineStyle are also not null.
 * If [areaBaseline] is an [AreaBaseline.ArbitraryLine] then the size of the line data must be equal to that of
 * [data], and their x-axis values must match.
 * @param symbol Composable for the symbol to be shown at each data PointEntry.
 * @param modifier Modifier for the chart.
 */
@Composable
fun <X, Y> XYGraphScope<X, Y>.StairstepPlot(
    data: List<PointEntry<X, Y>>,
    lineStyle: LineStyle,
    modifier: Modifier = Modifier,
    symbol: SymbolSlate<PointEntry<X, Y>>? = null,
    areaStyle: AreaStyle? = null,
    areaBaseline: AreaBaseline<X, Y>? = null,
    animationSpec: AnimationSpec<Float> = KoalaPlotTheme.animationSpec,
    selected:Boolean = false
) {
    if (data.isEmpty()) return

    if (areaStyle != null) {
        require(areaBaseline != null) { "areaBaseline must be provided for area charts" }
        if (areaBaseline is AreaBaseline.ArbitraryLine) {
            require(areaBaseline.values.size == data.size) {
                "baseline values must be the same size as the data"
            }
        }
    }

    GeneralLinePlot(
        data,
        modifier,
        lineStyle,
        symbol,
        areaStyle,
        areaBaseline,
        animationSpec,
        selected = selected
    ) { points: List<PointEntry<X, Y>>, size: Size ->
        var lastPoint = points[0]
        var scaledLastPoint = scale(lastPoint, size)

        moveTo(scaledLastPoint)
        for (index in 1..points.lastIndex) {
            val midPoint = scale(PointEntry(x = points[index].x, y = lastPoint.y), size)
            lineTo(midPoint)
            lastPoint = points[index]
            scaledLastPoint = scale(lastPoint, size)
            lineTo(scaledLastPoint)
        }
    }
}

/**
 * A [StairstepPlot] that differentiate [lineStyle] & [areaBaseline] at each [Y]-values based on [levelLineStyle].
 * @param X The type of the x-axis values
 * @param Y The type of the y-axis values
 * @param data Data series to plot.
 * @param lineStyle Style to use for the line that connects the data points.
 * @param levelLineStyle Style to use for emphasizing the y-axis values. (Used for line that connects same-level
 *  data points, data that have same value ([Y]) should have the same style).
 * @param cap Choose the [StrokeCap] used for level lines ending.
 * @param areaStyle Style to use for filling the area between the line and the 0-cross of the y-axis, or the
 *  y-axis value closest to 0 if the axis does not include 0. If null, no area will be drawn.
 *  [lineStyle] must also be non-null for the area to be drawn.
 * each PointEntry having the same x-axis value.
 * @param areaBaseline Baseline location for the area. Must be not be null if areaStyle and lineStyle are also not null.
 * If [areaBaseline] is an [AreaBaseline.ArbitraryLine] then the size of the line data must be equal to that of
 * [data], and their x-axis values must match.
 * @param symbol Composable for the symbol to be shown at each data PointEntry.
 * @param modifier Modifier for the chart.
 */
@Composable
fun <X, Y> XYGraphScope<X, Y>.StairstepPlot(
    data: List<PointEntry<X, Y>>,
    lineStyle: LineStyle,
    levelLineStyle: (Y) -> LineStyle,
    cap: StrokeCap = StrokeCap.Square,
    modifier: Modifier = Modifier,
    symbol: SymbolSlate<PointEntry<X, Y>>? = null,
    areaStyle: ((Y) -> AreaStyle)? = null,
    areaBaseline: AreaBaseline<X, Y>? = null,
    animationSpec: AnimationSpec<Float> = KoalaPlotTheme.animationSpec,
    selected:Boolean = false
) {
    if (data.isEmpty()) return

    if (areaStyle != null) {
        require(areaBaseline != null) { "areaBaseline must be provided for area charts" }
        if (areaBaseline is AreaBaseline.ArbitraryLine) {
            require(areaBaseline.values.size == data.size) {
                "baseline values must be the same size as the data"
            }
        }
    }

    // Modified version of [GeneralLinePlot].

    // Animation scale factor
    val beta = remember { Animatable(0f) }
    LaunchedEffect(null) { beta.animateTo(1f, animationSpec = animationSpec) }

    Layout(
        modifier = modifier.drawWithContent {
            clipRect(right = size.width * beta.value) { (this@drawWithContent).drawContent() }
        },
        content = {
            Canvas(modifier = Modifier.fillMaxSize()) {


                /** Order of executing: [onFirstPoint] -> [onMidPoint] -> [onNextPoint] -> [onMidPoint] ...,
                so `nextPoint` of [onNextPoint] will becomes `lastPoint` of [onMidPoint]. */
                /** Order of executing: [onFirstPoint] -> [onMidPoint] -> [onNextPoint] -> [onMidPoint] ...,
                so `nextPoint` of [onNextPoint] will becomes `lastPoint` of [onMidPoint]. */
                /** Order of executing: [onFirstPoint] -> [onMidPoint] -> [onNextPoint] -> [onMidPoint] ...,
                so `nextPoint` of [onNextPoint] will becomes `lastPoint` of [onMidPoint]. */

                /** Order of executing: [onFirstPoint] -> [onMidPoint] -> [onNextPoint] -> [onMidPoint] ...,
                so `nextPoint` of [onNextPoint] will becomes `lastPoint` of [onMidPoint]. */
                fun scaledPointsVisitor(
                    points: List<PointEntry<X, Y>>,
                    onFirstPoint: (OffsetPoint<X, Y>) -> Unit = { _ -> },
                    onMidPoint: (lastPoint: OffsetPoint<X, Y>, midPoint: OffsetPoint<X, Y>) -> Unit = { _, _ -> },
                    onNextPoint: (midPoint: OffsetPoint<X, Y>, nextPoint: OffsetPoint<X, Y>) -> Unit = { _, _ -> },
                ) {
                    var lastPoint = points[0]
                    var scaledLastPoint = scale(lastPoint, size)
                    var offsetLastPoint = OffsetPoint(scaledLastPoint, lastPoint)

                    onFirstPoint(offsetLastPoint)
                    for (index in 1..points.lastIndex) {
                        val scaledMidPoint =
                            scale(PointEntry(x = points[index].x, y = lastPoint.y), size)
                        val midPoint = OffsetPoint(scaledMidPoint, lastPoint)
                        onMidPoint(offsetLastPoint, midPoint)
                        lastPoint = points[index]
                        scaledLastPoint = scale(lastPoint, size)
                        offsetLastPoint = OffsetPoint(scaledLastPoint, lastPoint)
                        onNextPoint(midPoint, offsetLastPoint)
                    }
                }
                if (areaBaseline != null && areaStyle != null) {
                    var i = 0
                    var lastPoint: OffsetPoint<X, Y>? = null
                    scaledPointsVisitor(
                        data,
                        onMidPoint = { lp, _ -> lastPoint = lp },
                        onNextPoint = { midPoint, nextPoint ->
                            fillRectangle(
                                leftTop = lastPoint!!.offset,
                                rightBottom = scale(
                                    PointEntry(
                                        nextPoint.PointEntry.x,
                                        when (areaBaseline) {
                                            is AreaBaseline.ConstantLine -> areaBaseline.value

                                            is AreaBaseline.ArbitraryLine -> areaBaseline.values[i].y
                                        },
                                    ),
                                    size
                                ),
                                areaStyle = areaStyle(midPoint.PointEntry.y)
                            )
                            i++
                        }
                    )
                }

                // draw vertical lines using lineStyle
                scaledPointsVisitor(
                    data,
                    onNextPoint = { midPoint, p ->
                        with(lineStyle) {
                            drawLine(
                                brush,
                                midPoint.offset,
                                p.offset,
                                strokeWidth.toPx(),
                                Stroke.DefaultCap,
                                pathEffect,
                                alpha,
                                colorFilter,
                                blendMode
                            )
                        }
                    }
                )
                // draw horizontal lines using levelLineStyle()
                scaledPointsVisitor(
                    data,
                    onMidPoint = { lastPoint, p ->
                        with(levelLineStyle(p.PointEntry.y)) {
                            drawLine(
                                brush,
                                lastPoint.offset,
                                p.offset,
                                strokeWidth.toPx(),
                                cap,
                                pathEffect,
                                alpha
                            )
                        }
                    },
                )
            }
            if(symbol != null)
              Symbols(data, selectCheck = { selected }, symbol = symbol)
        }
    ) { measurables: List<Measurable>, constraints: Constraints ->
        layout(constraints.maxWidth, constraints.maxHeight) {
            measurables.forEach {
                it.measure(constraints).place(0, 0)
            }
        }
    }
}

